#include "shader.h"
#include <GL/glew.h>
#include <iostream>
#include <fstream>


namespace kanon
{
    std::string GetShaderTypeString(ShaderSourceType type)
    {
        if (type == ShaderSourceType::SHADER_TYPE_FRAGMENT)
            return "SHADER_TYPE_FRAGMENT";
        else if (type == ShaderSourceType::SHADER_TYPE_VERTEX)
            return "SHADER_TYPE_VERTEX";
        else
            return "SHADER_TYPE_NONE";
    }

    std::shared_ptr<Shader> Shader::FromSources(const std::string& vertex_shader, const std::string& fragment_shader)
    {
        std::shared_ptr<Shader> shader = std::make_shared<Shader>();
        uint32_t vshader = shader->LoadShaderSource(vertex_shader, ShaderSourceType::SHADER_TYPE_VERTEX);
        uint32_t fshader = shader->LoadShaderSource(fragment_shader, ShaderSourceType::SHADER_TYPE_FRAGMENT);

        shader->LinkProgram(vshader, fshader);
        return shader;
    }

    std::shared_ptr<Shader> Shader::FromShaderFiles(const std::string& vertex_path, const std::string& fragment_path)
    {
        std::vector<char> vertex_source;
        std::vector<char> fragment_source;

        std::shared_ptr<Shader> shader = std::make_shared<Shader>();
        shader->LoadFileContent(vertex_path, vertex_source);
        shader->LoadFileContent(fragment_path, fragment_source);

        int32_t vshader = shader->LoadShaderSource(vertex_source.data(), ShaderSourceType::SHADER_TYPE_VERTEX);
        if (vshader < 0)
        {
            std::cout << "Failed to load vertex shader: " << vertex_path << std::endl;
            return nullptr;
        }
        int32_t fshader = shader->LoadShaderSource(fragment_source.data(), ShaderSourceType::SHADER_TYPE_FRAGMENT);
        if (fshader < 0)
        {
            std::cout << "Failed to load fragment shader: " << fragment_path << std::endl;
            return nullptr;
        }
        int32_t link_res = shader->LinkProgram(vshader, fshader);
        if (link_res < 0)
            return nullptr;
        return shader;
    }

    Shader::Shader()
    {
        shader_id = glCreateProgram();
    }

    Shader::~Shader()
    {
        glDeleteProgram(shader_id);
    }

    void Shader::Bind()
    {
        glUseProgram(shader_id);
    }

    void Shader::Unbind()
    {
        glUseProgram(0);
    }

    void Shader::set_matrix(const std::string& name, const glm::mat4& m) const
    {
        GLint location = glGetUniformLocation(shader_id, name.c_str());
        glUniformMatrix4fv(location, 1, GL_FALSE, &m[0][0]);
    }

    void Shader::set_texture_slot(const std::string& name, int slot) const
    {
        GLint location = glGetUniformLocation(shader_id, name.c_str());
        glUniform1i(location, slot);
    }

    void Shader::set_texture_array(const std::string& name, int* slots, int size) const
    {
        GLint location = glGetUniformLocation(shader_id, name.c_str());
        glUniform1iv(location, size, slots);
    }

    int32_t Shader::LoadShaderSource(const std::string& shader_source, ShaderSourceType shader_type)
    {
        int32_t shader = -1;
        if (shader_type == ShaderSourceType::SHADER_TYPE_VERTEX)
            shader = glCreateShader(GL_VERTEX_SHADER);
        else if (shader_type == ShaderSourceType::SHADER_TYPE_FRAGMENT)
            shader = glCreateShader(GL_FRAGMENT_SHADER);

        if (shader == -1)
            return -1;

        const char* shader_source_c = shader_source.c_str();
        glShaderSource(shader, 1, &shader_source_c, NULL);
        glCompileShader(shader);

        int success;
        char infoLog[512];
        glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
        if (!success) 
        {
            glGetShaderInfoLog(shader, 512, NULL, infoLog);
            std::cerr << "ERROR::SHADER::" << GetShaderTypeString(shader_type) << "::COMPILATION_FAILED\n" << infoLog << std::endl;
            return -1;
        }
        return shader;
    }

    int32_t Shader::LinkProgram(uint32_t vertex_shader, uint32_t fragment_shader)
    {
        glAttachShader(shader_id, vertex_shader);
        glAttachShader(shader_id, fragment_shader);
        glLinkProgram(shader_id);

        int success;
        char infoLog[512];

        glGetProgramiv(shader_id, GL_LINK_STATUS, &success);
        if (!success) {
            glGetProgramInfoLog(shader_id, 512, NULL, infoLog);
            std::cerr << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
            return -1;
        }

        glDeleteShader(vertex_shader);
        glDeleteShader(fragment_shader);

        return 0;
    }

    bool Shader::LoadFileContent(const std::string& filepath, std::vector<char>& source)
    {
        FILE* fp = fopen(filepath.c_str(), "r");
        if (fp == nullptr)
        {
            printf("cannot open %s\n", filepath.c_str());
            return false;
        }
        fseek(fp, 0, SEEK_END);
        uint64_t filesize = ftell(fp);
        source.resize(filesize);
        fseek(fp, 0, SEEK_SET);
        fread(source.data(), sizeof(char), filesize, fp);
        return true;
    }
}
